1"use client";
2
3
18import { useMemo } from "react";
19import { useParams } from "next/navigation";
20import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
21import { Database as DatabaseIcon, FileText, Loader2, RotateCcw, Trash2 } from "lucide-react";
22import { formatRelativeTime } from "@/lib/utils";
23import { apiFetch, pageApi } from "@/lib/api";
24import { useWorkspace } from "@/hooks/use-workspace";
25import { Page } from "@/lib/types";
26
27export default function TrashPage() {
28 const params = useParams<{ workspace: string }>();
29 const slugOrId = params?.workspace ?? "";
30 const { data: workspace } = useWorkspace(slugOrId);
31 const qc = useQueryClient();
32
33 const trashed = useQuery({
34 queryKey: ["workspaces", workspace?.id ?? slugOrId, "trash"],
35 queryFn: () => pageApi.list(workspace?.id ?? slugOrId, { trashed: true, limit: 200 }),
36 enabled: !!workspace?.id,
37 retry: false
38 });
39
40
41
42 const refresh = () => {
43 qc.invalidateQueries({ queryKey: ["workspaces", workspace?.id, "trash"] });
44 qc.invalidateQueries({ queryKey: ["workspaces", workspace?.id, "page-tree"] });
45 };
46
47 const restore = useMutation({
48 mutationFn: (pageId: string) => pageApi.restore(pageId),
49 onSuccess: refresh
50 });
51
52 const purge = useMutation({
53 mutationFn: async (pageId: string): Promise<void> => {
54 await apiFetch(`/pages/${pageId}/permanent`, { method: "DELETE" });
55 },
56 onSuccess: refresh
57 });
58
59
60
61
62 const role = (workspace?.role ?? "").toLowerCase();
63 const canPurge = role === "owner" || role === "admin";
64
65
66 const items = useMemo(
67 () =>
68 [...(trashed.data ?? [])].sort(
69 (a, b) => new Date(b.trashedAt ?? 0).getTime() - new Date(a.trashedAt ?? 0).getTime()
70 ),
71 [trashed.data]
72 );
73
74 return (
75 <div className="mx-auto max-w-2xl px-6 py-8">
76 <h1 className="mb-1 text-xl font-semibold">Trash</h1>
77 <p className="mb-4 text-sm text-muted-foreground">
78 Deleted pages are kept here. Restore to bring them back, or purge to delete permanently.
79 </p>
80
81 {trashed.isLoading && (
82 <p className="inline-flex items-center gap-2 text-sm text-muted-foreground">
83 <Loader2 className="size-4 animate-spin" /> Loading trash…
84 </p>
85 )}
86
87 {trashed.isError && (
88 <p className="rounded-md border border-dashed border-border p-4 text-sm text-muted-foreground">
89 Couldn't load the trash. Viewing the trash requires the Admin or Owner role.
90 </p>
91 )}
92
93 {!trashed.isLoading && !trashed.isError && items.length === 0 && (
94 <p className="rounded-md border border-dashed border-border p-6 text-center text-sm text-muted-foreground">
95 The trash is empty.
96 </p>
97 )}
98
99 {}
102 <ul data-testid="trash-list" className="space-y-1.5">
103 {items.map((p) => (
104 <li key={p.id} data-testid={`trash-item-${p.id}`} className="flex items-center gap-2.5 rounded-md border border-border px-3 py-2">
105 <span className="shrink-0 text-sm" aria-hidden>
106 {p.icon || (p.databaseId ? <DatabaseIcon className="size-4 text-blue-600" /> : <FileText className="size-4 text-muted-foreground" />)}
107 </span>
108 <div className="min-w-0 flex-1">
109 <p className="truncate text-sm">{p.title || "Untitled"}</p>
110 <p className="text-[11px] text-muted-foreground">
111 Trashed {p.trashedAt ? formatRelativeTime(p.trashedAt) : "recently"}
112 </p>
113 </div>
114 <button
115 type="button"
116 data-testid={`trash-restore-${p.id}`}
117 className="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs text-muted-foreground hover:bg-muted disabled:opacity-50"
118 onClick={() => restore.mutate(p.id)}
119 disabled={restore.isPending}
120 >
121 <RotateCcw className="size-3.5" aria-hidden /> Restore
122 </button>
123 {canPurge && (
124 <button
125 type="button"
126 data-testid={`trash-purge-${p.id}`}
127 className="inline-flex items-center gap-1 rounded-md border border-destructive/40 px-2 py-1 text-xs text-destructive hover:bg-destructive/10 disabled:opacity-50"
128
129
130
131
132
133
134 onClick={() => purge.mutate(p.id)}
135 disabled={purge.isPending}
136 >
137 <Trash2 className="size-3.5" aria-hidden /> Purge
138 </button>
139 )}
140 </li>
141 ))}
142 </ul>
143 </div>
144 );
145}
146